/*
* Copyright (c) 2009, Active Web Solutions Limited
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1- Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2- Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3- The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
*
* THIS SOFTWARE IS PROVIDED BY Active Web Solutions Limited ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL Active Web Solutions Limited BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package net.aws.jazure;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import net.aws.jazure.exception.APIRequestFailedException;
import net.aws.jazure.httpconstants.CompConstants;
import net.aws.jazure.httpconstants.HeaderNames;
import net.aws.jazure.httpconstants.ListingConstants;
import net.aws.jazure.httpconstants.QueryParams;
import net.aws.jazure.httpconstants.XMLElements;
import net.aws.jazure.util.ISO8601Helper;
import net.aws.jazure.util.RestHelper;

import org.apache.log4j.Logger;
import org.restlet.Client;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;
import org.restlet.representation.InputRepresentation;
import org.restlet.representation.StringRepresentation;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * <p>The Blob container class.</p>
 * <p>Used to access and list blobs in the container.</p>
 * 
 * @author dyaa.albakour
 *
 */
public class BlobContainer {	
	
	private String accountName;	
	private String accountSharedKey;
	private String endPoint;
	
	private String containerName;
	private String url;
	private Calendar lastModefiedDate;
	
	/**
	 * A constructor with default access modifier.
	 * For package use only 
	 * 
	 * @param endPoint The base Uri for the storage endpoint.
	 * @param containerName Name of the container.
	 * @param accountName Name of the storage account.
	 * @param accountSharedKey The Account shared key.
	 */
	BlobContainer(String endPoint, String containerName,
						 String accountName, String accountSharedKey )
	{
		super();
		this.accountName = accountName;
		this.containerName = containerName;
		this.accountSharedKey = accountSharedKey;
		this.endPoint = endPoint;
	}
	
	
	/**
	 * <p>Create the container if it doesn't exist.</p>
	 * <p>The container is created with private access control and no meta data.</p>
	 * @return true if the container was created. false it the container already exists.
	 * @throws APIRequestFailedException If the API request return an unexpected HTTP status code.
	 */
	public boolean create()
		throws APIRequestFailedException{
		
		
		Client client = new Client(Protocol.HTTP); 
		ChallengeResponse challengeResponse =  new ChallengeResponse(
	    	ChallengeScheme.HTTP_MS_SHAREDKEY,
	    	accountName,
	    	accountSharedKey);
		
		
		String url = endPoint + "/" + containerName;
		Request request = new Request(Method.PUT, url);		
		
		request.setChallengeResponse(challengeResponse);		
		
		// RESTlet seems not to be setting the content length of the request
		// 	with the PUT method and no content.
		// Azure Queue service is complaining about content length being not specified
		// To overcome this I add to the request a dummy string..	
		request.setEntity(new StringRepresentation("dummy"));	
		
		Response response = client.handle(request);		
		// The creation succeeds if the HTTP response was 201 ( success created).
		Status responseStatus = response.getStatus();
		if(responseStatus.equals(Status.SUCCESS_CREATED)){
			return true;
		}else if(responseStatus.equals(Status.CLIENT_ERROR_CONFLICT)){
			return false;
		}else
			throw new APIRequestFailedException(responseStatus);
		
	}
	
	/**
	 * <P>Create the container with the specified metadata and access control if it does not exist</p>
	 * @param metaData The metadata for the container
	 * @param acl The access control (public or private) with which to create the container.
	 * @return true if the container was created. false it the container already exists.
	 */
	public boolean create(Map<String,String> metaData, ContainerAccessControl acl){
		//TODO implement!
		return false;
	}
	
	/**
	 * <p>Deletes a blob with a given name.</p>
	 * @param blobName the name of the blob
	 * @return true if the blob exist and was successfully deleted, false if the blob doesn't exist.
	 * 
	 * @throws APIRequestFailedException If the API request return an unexpected HTTP status code.
	 */
	public boolean deleteBlob(String blobName) 
		throws APIRequestFailedException{
		boolean result = false;
		
		Client client = new Client(Protocol.HTTP); 
		ChallengeResponse challengeResponse =  new ChallengeResponse(
	    	ChallengeScheme.HTTP_MS_SHAREDKEY,
	    	accountName,
	    	accountSharedKey);
		
		String url = endPoint + "/" + containerName + "/" +blobName;
		Request request = new Request(Method.DELETE, url);
		request.setChallengeResponse(challengeResponse);
		Response response = client.handle(request); 
		
		Status responseStatus = response.getStatus();
		
		if(responseStatus.equals(Status.SUCCESS_OK)|| 
		   responseStatus.equals(Status.SUCCESS_ACCEPTED)){
			return true;
		}else if(responseStatus.equals(Status.CLIENT_ERROR_NOT_FOUND)|| 
		  responseStatus.equals(Status.CLIENT_ERROR_NOT_FOUND)){
			return false;
		}else
			throw new APIRequestFailedException(responseStatus);
		

	}
	
	/**
	 * <p>Deletes the current container.</p>
	 * @return true if the container is successfully deleted, false if the container doesn't exist.
	 * 
	 */
	public boolean delete()
		throws APIRequestFailedException{
		boolean result = false;
		
		Client client = new Client(Protocol.HTTP); 
		ChallengeResponse challengeResponse =  new ChallengeResponse(
	    	ChallengeScheme.HTTP_MS_SHAREDKEY,
	    	accountName,
	    	accountSharedKey);
		
		String url = endPoint + "/" + containerName ;
		Request request = new Request(Method.DELETE, url);
		request.setChallengeResponse(challengeResponse);
		Response response = client.handle(request); 
		
		Status responseStatus = response.getStatus();
		
		if(responseStatus.equals(Status.SUCCESS_OK)|| 
		   responseStatus.equals(Status.SUCCESS_ACCEPTED)){
			return true;
		}else if(responseStatus.equals(Status.CLIENT_ERROR_NOT_FOUND)|| 
		  responseStatus.equals(Status.CLIENT_ERROR_NOT_FOUND)){
			return false;
		}else
			throw new APIRequestFailedException(responseStatus);
		
	}
	
	/**
	 * Get the blob contents and properties if the blob exists.
	 * 
	 * @param blobName The name of the blob
	 * @param stream An outputStream in which the contents are read.	
	 * @return the properties of the blob if it exists.
	 * @throws APIRequestFailedException
	 */
	public BlobProperties getBlobImpl(String blobName, OutputStream outStream)
		throws APIRequestFailedException{
		
		// TODO check if the stream is null, to retrieve only BlobProperties 
		try{
			
			Client client = new Client(Protocol.HTTP); 
			ChallengeResponse challengeResponse =  new ChallengeResponse( ChallengeScheme.HTTP_MS_SHAREDKEY,
										      accountName,
										      accountSharedKey);
			
			String url = endPoint + "/" + containerName + "/"+blobName;
			Request request = new Request(Method.GET, url);
			
			request.setChallengeResponse(challengeResponse);
			
			Response response = client.handle(request); 
			
			Status responseStatus = response.getStatus();
			if(responseStatus.equals(Status.SUCCESS_OK))
			{
				// write the content to the output stream
				InputStream is= response.getEntity().getStream();				
				int numRead;
				byte buf[] = new byte[1024];
				while ( (numRead = is.read(buf)) >= 0 )  {
			          outStream.write(buf, 0, numRead);
			    }
				outStream.flush();
				
				// TODO parse the BlobProperties from the response.
				return new BlobProperties(blobName);
			}
			else
				throw new APIRequestFailedException(responseStatus);
		} catch (IOException e) {
			return null;
		}
	}
	
	/**
	 * <p>Enumerates all blobs with a given prefix.<p>
	 * 
	 * @param prefix The prefix
	 * @param combineCommonPrefixes If true common prefixes with "/" as seperator.
	 * @return The list of blob properties and common prefixes
	 * @throws APIRequestFailedException  If the API request return an unexpected HTTP status code.
	 */
	public List<Object> listBlobs(String prefix,boolean combineCommonPrefixes) 
		throws APIRequestFailedException{
		List<Object> result = new ArrayList<Object>();
		
		String marker = "";
		final int maxResults = ListingConstants.MaxBlobListResults;
		String delimeter = combineCommonPrefixes?"/":"";
		do{
			ListBlobsResult res= listBlobImpl(prefix, marker, delimeter, maxResults);
			marker= res.getNextMarker();
			
			for(String commonprefix:res.getCommonPrefixes()){
				result.add(commonprefix);
			}
			for(BlobProperties blob:res.getBlobs()){
				result.add(blob);
			}
			
		}while(marker!=null);
		
		return result;
	}
	
	
	/**
	 * Makes a call to the Azure Rest API to list the Blobs in the current container
	 * 
	 * @param Optional(can be null). Filters the results to return only blobs whose names begin with the specified prefix.
	 * @param delimeter Optional(can be null). When the request includes this parameter, the operation returns a BlobPrefix element in the 
	 * 					response body that acts as a placeholder for all blobs whose names begin with the same 
	 *					substring up to the appearance of the delimiter character. The delimiter may be a single character or a string.
	 * @param fromMarker Optional(can be null). A string value that identifies the portion of the list to be returned with the next list operation. 
	 * 							The operation returns a marker value within the response body if the list returned was not complete. 
	 * 							The marker value may then be used in a subsequent call to request the next set of list items.
	 * 							The marker value is opaque to the client. 
	 * @param maxCount Optional(can be null). Specifies the maximum number of blobs to return, including all BlobPrefix elements.
	 *  						If maxresults is not specified, the server will return up to 5,000 items.
	 *  						Setting maxresults to a value greater than 5,000 results in error response code 400 (Bad Request).
	 * @return A <code>ListBlobsResult</code> instance if the request succeeds or null if an
	 * 			error occured in parsing the XML response. 			
	 * @throws APIRequestFailedException If the API request return an unexpected HTTP status code.
	 */
	private ListBlobsResult listBlobImpl(String prefix, String fromMarker, String delimeter, int maxCount)
		throws APIRequestFailedException{
		
		try{
			Client client = new Client(Protocol.HTTP); 
			ChallengeResponse challengeResponse =  new ChallengeResponse(
		    	ChallengeScheme.HTTP_MS_SHAREDKEY,
		    	accountName,
		    	accountSharedKey);			
			
			// Constructing the request URL 
			Map<String, String> parameters= new HashMap<String, String>();
			parameters.put(QueryParams.QueryParamComp, CompConstants.List);
	
			if(prefix!=null){
				if(!prefix.isEmpty())
					parameters.put(QueryParams.QueryParamPrefix,prefix);
			}
			
			if(fromMarker!=null){
				if(!fromMarker.isEmpty())
					parameters.put(QueryParams.QueryParamMarker,fromMarker);
			}
			
			if(delimeter!=null){
				if(!delimeter.isEmpty())
					parameters.put(QueryParams.QueryParamDelimiter,delimeter);
			}
			
			parameters.put(QueryParams.QueryParamMaxResults,Integer.toString(maxCount));					
			String url = RestHelper.createRequestURI(endPoint+"/" + this.containerName ,parameters);	
			Request request = new Request(Method.GET, url);		
			request.setChallengeResponse(challengeResponse);			
			Response response = client.handle(request);
			
			if( response.getStatus().equals(Status.SUCCESS_OK) ){
				return listBlobsResultFromResponse(response.getEntity().getStream());
			}else
				throw new APIRequestFailedException(response.getStatus());
			
		} catch (UnsupportedEncodingException e) {
			// createRequestURI failed
			return null;
		} catch (IOException e) {
			// response.getEntity().getStream()
			return null;
		} catch (XPathExpressionException e) {
			return null;
		} catch (SAXException e) {
			return null;
		} catch (ParserConfigurationException e) {
			return null;
		}
	} 
	
	/**
	 * Parse the XML response from ListBlobs.
	 * 
	 * @param stream The InputStream to parse
	 * @return A <code>ListBlobsResult</code> instance
	 * @throws IOException 
	 * @throws ParserConfigurationException 
	 * @throws SAXException 
	 * @throws XPathExpressionException 
	 */
	private ListBlobsResult listBlobsResultFromResponse(InputStream stream)
		throws IOException, SAXException, ParserConfigurationException, XPathExpressionException{		
		
		//TODO check for null values (when the xml nodes do not exist).
		
		List<BlobProperties> blobs = new ArrayList<BlobProperties>();		
		List<String> commonPrefixes = new ArrayList<String>();		
		String nextMarker = null;
		
		BufferedReader rd = new BufferedReader(new InputStreamReader(stream, "UTF-8") );			
		rd.skip(1);	        
		
		// Parse the response as an XML document
		Document document=
			DocumentBuilderFactory.newInstance().newDocumentBuilder().
			parse(new InputSource(rd));			
		
		// Now find the Blob nodes
		XPath xp= XPathFactory.newInstance().newXPath();
		
		NodeList prefixNodeList = (NodeList) xp.
				evaluate("/"+XMLElements.EnumerationResults +
						"/"+XMLElements.Blobs +
						"/"+XMLElements.BlobPrefix,
						document, XPathConstants.NODESET);				
		
		for(int i=0;i<prefixNodeList.getLength();i++){
			Node prefixNode = prefixNodeList.item(i);
			commonPrefixes.add(prefixNode.getTextContent());
		}
		
		
		NodeList blobNodelist=(NodeList) xp.
				evaluate("/"+XMLElements.EnumerationResults +
						 "/"+XMLElements.Blobs +
						 "/"+XMLElements.Blob,
						 document, XPathConstants.NODESET);			
		
		// Retrieve each Blob Properties
		for(int i=0;i<blobNodelist.getLength();i++){			
			
			Node blobNode = blobNodelist.item(i);				
			
			XPath xpath= XPathFactory.newInstance().newXPath();
			
			// retrieve last modified date				
			Node lastModifiedNode=(Node) xpath.evaluate(XMLElements.LastModified,
					blobNode, XPathConstants.NODE);
			
			Calendar lastModified=  Calendar.getInstance();
			lastModified.setTime(ISO8601Helper.parseISO8061(lastModifiedNode.getTextContent()));
					
			
			// Etag
			Node etagNode=(Node) xp.evaluate(XMLElements.Etag,
					blobNode, XPathConstants.NODE);
			String etag = new String (etagNode.getTextContent());
			
			//content type
			Node contentTypeNode=(Node) xp.evaluate(XMLElements.ContentType,
					blobNode, XPathConstants.NODE);
			String contentType = new String (contentTypeNode.getTextContent());
			
			//content Encoding
			Node contentEncodingNode=(Node) xp.evaluate(XMLElements.ContentEncoding,
					blobNode, XPathConstants.NODE);
			String contentEncoding = new String (contentEncodingNode.getTextContent());
			
			//content Language
			Node contentLanguageNode=(Node) xp.evaluate(XMLElements.ContentLanguage,
					blobNode, XPathConstants.NODE);
			String contentLanguage = new String (contentLanguageNode.getTextContent());
			
			// size
			Node sizeNode =(Node) xp.evaluate(XMLElements.Size,
					blobNode, XPathConstants.NODE);
			long blobSize = Long.parseLong(sizeNode.getTextContent());
			
			// Blob URL
			Node urlNode =(Node) xp.evaluate(XMLElements.Url,
					blobNode, XPathConstants.NODE);
			String blobUrl = new String (urlNode.getTextContent());

			// blob Name
			Node nameNode =(Node) xp.evaluate(XMLElements.BlobName,
					blobNode, XPathConstants.NODE);
			String blobName = new String (nameNode.getTextContent());
			
			// now create a new BlobProperties object
			BlobProperties properties = new BlobProperties(blobName);
			properties.setLastModifiedTime(lastModified);
			properties.setETag(etag);
			properties.setContentType(contentType);
			properties.setContentEncoding(contentEncoding);
			properties.setContentLanguage(contentLanguage);				
			properties.setContentLength(blobSize);
			
			blobs.add(properties);
		}
		
		Node nextMarkerNode = (Node) xp.
							evaluate("/"+XMLElements.EnumerationResults+
									 "/"+XMLElements.NextMarker,
									 document, XPathConstants.NODE);
		Logger.getRootLogger().info(nextMarkerNode.getTextContent());
		if(nextMarkerNode.getFirstChild()!=null)
			nextMarker = nextMarkerNode.getTextContent();		
		
		return new ListBlobsResult(blobs,commonPrefixes,nextMarker);
	}
	
	/**
	 * @param properties
	 * @param contents
	 * @param overwrite
	 * @return
	 */
//	public boolean createBlob(BlobProperties properties,BlobContents contents, boolean overwrite){
//		
//		return putBlobImpl( properties, contents.asInputStream(), overwrite);
//	}
	
	/**
	 * This method uses the Azure Storage REST API to put a blob
	 * in a single transaction. 
	 * 
	 * @param properties the properties of the blob.
	 * @param is the contents of the blob.
	 * @param overwrite should this request overwrite an existing blob ?
	 * @return true if the blob was created.<br/> 
	 * 		   false if the blob already exists and overwrite was set to false.
	 * 		
	 */
	public boolean putBlobImpl(BlobProperties properties, InputStream is, boolean overwrite )
			throws APIRequestFailedException{
		
		// TODO implement createBlob method as in the StorageClient example where the Block			
		// 		uploading is implicit to the user of the API		
		boolean result = false;
		
		Client client = new Client(Protocol.HTTP); 
		ChallengeResponse challengeResponse =  new ChallengeResponse( ChallengeScheme.HTTP_MS_SHAREDKEY,
									      accountName,
									      accountSharedKey);
		
		String url = endPoint + "/" + containerName + "/"+properties.getName();
		Request request = new Request(Method.PUT, url);
		
		request.setChallengeResponse(challengeResponse);
		
		// add the meta-data to be set in the headers.
		// Note that the prefix should be added to the name of the header.
		Form headers = new Form();
		for(String key: properties.getMetaData().keySet()){
			headers.add(HeaderNames.PrefixForMetadata + key,properties.getMetaData().get(key) );
		}
		request.getAttributes().put("org.restlet.http.headers",headers);		
		
		InputRepresentation ir= new InputRepresentation(is, new MediaType(properties.getContentType()));
		
		try {
			ir.setSize(is.available());
		} catch (IOException e) {
			// TODO handle this sort of exceptions
			Logger.getRootLogger().error("Error in calculating the Blob size", e);
		}		
		
		// TODO set the content-Encoding and content-Language headers		
		request.setEntity(ir);
		
		// send the request
		Response response = client.handle(request);
		
		Logger.getRootLogger().debug(response.getEntityAsText());
		
		Status responseStatus = response.getStatus();
		if(responseStatus.equals(Status.SUCCESS_CREATED)){
			result = true;
		}else if( !overwrite && 
					(  responseStatus.equals(Status.CLIENT_ERROR_PRECONDITION_FAILED) ||
					   responseStatus.equals(Status.REDIRECTION_NOT_MODIFIED)	) ){
			result = false;
		}else{
			throw new APIRequestFailedException();
		}
		
		return result;
		
	}
	
	
	
	
	/**
	 * An internal representation for the blob list returned by the 
	 * RESTful Azure API
	 * 
	 * @author dyaa.albakour
	 *
	 */
	class ListBlobsResult{		
		
		List<BlobProperties> blobs;
		List<String> commonPrefixes;
		String nextMarker;
		
		ListBlobsResult(List<BlobProperties> blobs, List<String> commonPrefixes, String nextMarker) {
			this.blobs = blobs;
			this.commonPrefixes = commonPrefixes;
			this.nextMarker= nextMarker;
		}
		
		List<BlobProperties> getBlobs() {
			return blobs;
		}		
		List<String> getCommonPrefixes() {
			return commonPrefixes;
		}

		String getNextMarker() {
			return nextMarker;
		}
		
		
	}
	
	
	
	
}
